/* This file is part of NanoXML.
 *
 * $Revision: 1.17 $
 * $Date: 2000/09/04 20:26:55 $
 * $Name: RELEASE_1_6_4 $
 *
 * Copyright (C) 2000 Marc De Scheemaecker, All Rights Reserved.
 *
 * This software is provided 'as-is', without any express or implied warranty.
 * In no event will the authors be held liable for any damages arising from the
 * use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 *  1. The origin of this software must not be misrepresented; you must not
 *     claim that you wrote the original software. If you use this software in
 *     a product, an acknowledgment in the product documentation would be
 *     appreciated but is not required.
 *
 *  2. Altered source versions must be plainly marked as such, and must not be
 *     misrepresented as being the original software.
 *
 *  3. This notice may not be removed or altered from any source distribution.
 */
package XmlUtils;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

/**
 * kXMLElement is a representation of an XML object. The object is able to parse
 * XML code.
 * <P>
 * Note that NanoXML is not 100% XML 1.0 compliant:
 * <UL><LI>The parser is non-validating.
 *     <LI>The DTD is fully ignored, including <CODE>&lt;!ENTITY...&gt;</CODE>.
 *     <LI>There is no support for mixed content (elements containing both
 *         subelements and CDATA elements)
 * </UL>
 * <P>
 * You can opt to use a SAX compatible API, by including both
 * <CODE>nanoxml.jar</CODE> and <CODE>nanoxml-sax.jar</CODE> in your classpath
 * and setting the property <CODE>org.xml.sax.parser</CODE> to
 * <CODE>nanoxml.sax.SAXParser</CODE>
 * <P>
 * $Revision: 1.17 $<BR>
 * $Date: 2000/09/04 20:26:55 $<P>
 *
 * @see nanoxml.Exception
 *
 * @author Marc De Scheemaecker
 *         &lt;<A HREF="mailto:Marc.DeScheemaecker@advalvas.be"
 *         >Marc.DeScheemaecker@advalvas.be</A>&gt;
 * @version 1.6
 */
public class kXMLElement
{

    public static class FakeProperties extends Hashtable
    {

        public FakeProperties()
        {
        }

        public String getProperty(String key)
        {
            return (String) super.get(key);
        }

        public String getProperty(String key, String defstr)
        {
            String val = getProperty(key);
            return ((val != null) ? val : defstr);
        }
    }
    /**
     * The attributes given to the object.
     */
    private FakeProperties attributes;
    /**
     * Subobjects of the object. The subobjects are of class kXMLElement
     * themselves.
     */
    private Vector children;
    /**
     * The class of the object (the name indicated in the tag).
     */
    private String tagName;
    /**
     * The #PCDATA content of the object. If there is no such content, this
     * field is null.
     */
    private String contents;
    /**
     * Conversion table for &amp;...; tags.
     */
    private FakeProperties conversionTable;
    /**
     * Whether to skip leading whitespace in CDATA.
     */
    private boolean skipLeadingWhitespace;
    /**
     * Whether the parsing is case sensitive.
     */
    private boolean ignoreCase;

    /**
     * Creates a new XML element. The following settings are used:
     * <DL><DT>Conversion table</DT>
     *     <DD>Minimal XML conversions: <CODE>&amp;amp; &amp;lt; &amp;gt;
     *         &amp;apos; &amp;quot;</CODE></DD>
     *     <DT>Skip whitespace in contents</DT>
     *     <DD><CODE>false</CODE></DD>
     *     <DT>Ignore Case</DT>
     *     <DD><CODE>true</CODE></DD>
     * </DL>
     *
     * @see nanoxml.kXMLElement#kXMLElement(FakeProperties)
     * @see nanoxml.kXMLElement#kXMLElement(boolean)
     * @see nanoxml.kXMLElement#kXMLElement(FakeProperties,boolean)
     */
    public kXMLElement()
    {
        this(new FakeProperties(), false, true, true);
    }

    /**
     * Creates a new XML element. The following settings are used:
     * <DL><DT>Conversion table</DT>
     *     <DD><I>conversionTable</I>, eventually combined with the minimal XML
     *         conversions: <CODE>&amp;amp; &amp;lt; &amp;gt;
     *         &amp;apos; &amp;quot;</CODE>
     *         (depending on <I>fillBasicConversionTable</I>)</DD>
     *     <DT>Skip whitespace in contents</DT>
     *     <DD><I>skipLeadingWhitespace</I></DD>
     *     <DT>Ignore Case</DT>
     *     <DD><I>ignoreCase</I></DD>
     * </DL>
     * <P>
     * This constructor should <I>only</I> be called from kXMLElement itself
     * to create child elements.
     *
     * @see nanoxml.kXMLElement#kXMLElement()
     * @see nanoxml.kXMLElement#kXMLElement(boolean)
     * @see nanoxml.kXMLElement#kXMLElement(FakeProperties)
     * @see nanoxml.kXMLElement#kXMLElement(FakeProperties,boolean)
     */
    protected kXMLElement(FakeProperties conversionTable,
                          boolean skipLeadingWhitespace,
                          boolean fillBasicConversionTable,
                          boolean ignoreCase)
    {
        this.ignoreCase = ignoreCase;
        this.skipLeadingWhitespace = skipLeadingWhitespace;
        this.tagName = null;
        this.contents = "";
        this.attributes = new FakeProperties();
        this.children = new Vector();
        this.conversionTable = conversionTable;
        // this.lineNr = 0;

        if (fillBasicConversionTable)
        {
            this.conversionTable.put("lt", "<");
            this.conversionTable.put("gt", ">");
            this.conversionTable.put("quot", "\"");
            this.conversionTable.put("apos", "'");
            this.conversionTable.put("amp", "&");
        }
    }

    /**
     * Returns the subobjects of the object.
     */
    public Vector getChildren()
    {
        return this.children;
    }

    /**
     * Returns the #PCDATA content of the object. If there is no such content,
     * <CODE>null</CODE> is returned.
     */
    public String getContents()
    {
        return this.contents;
    }

    /**
     * Returns a property of the object.
     * If the property doesn't exist, <I>defaultValue</I> is returned.
     */
    public String getProperty(String key,
                              String defaultValue)
    {
        if (this.ignoreCase)
        {
            key = key.toUpperCase();
        }

        return this.attributes.getProperty(key, defaultValue);
    }

    /**
     * Returns the class (i.e. the name indicated in the tag) of the object.
     */
    public String getTagName()
    {
        return this.tagName;
    }

    /**
     * Checks whether a character may be part of an identifier.
     */
    private boolean isIdentifierChar(char ch)
    {
        return (((ch >= 'A') && (ch <= 'Z')) || ((ch >= 'a') && (ch <= 'z')) || ((ch >= '0') && (ch <= '9')) || (".-_:".indexOf(ch) >= 0));
    }

    /**
     * Parses an XML definition.
     *
     * @exception nanoxml.Exception
     *    if an error occured while parsing the string
     */
    public void parseString(String string)
            throws Exception
    {
        //System.out.println("XMLXMLXML1" + string.length());
        this.parseCharArray(string.toCharArray(), 0, string.length(), 1);

    }

    /**
     * Parses an XML definition starting at <I>offset</I>.
     *
     * @return the offset of the array following the XML data (<= end)
     *
     * @exception nanoxml.Exception
     *    if an error occured while parsing the array
     */
    public int parseCharArray(char[] input,
                              int offset,
                              int end,
                              int startingLineNr)
            throws Exception
    { //System.out.println("parseCharArrayLIOR2");
        int[] lineNr = new int[1];
        lineNr[0] = startingLineNr;
        return this.parseCharArray(input, offset, end, lineNr);
    }

    /**
     * Parses an XML definition starting at <I>offset</I>.
     *
     * @return the offset of the array following the XML data (<= end)
     *
     * @exception nanoxml.Exception
     *    if an error occured while parsing the array
     */
    private int parseCharArray(char[] input,
                               int offset,
                               int end,
                               int[] currentLineNr)
            throws Exception
    {
        //System.out.println("parseCharArrayLIOR4-1");
        //this.lineNr = currentLineNr[0];
        this.tagName = null;
        this.contents = "";
        this.attributes = new FakeProperties();
        this.children = new Vector();
        //System.out.println("parseCharArrayLIOR4-2");
        try
        {
            offset = this.skipWhitespace(input, offset, end, currentLineNr);
        }
        catch (Exception e)
        {

            //System.out.println("error=" + e);
            return offset;
        }
        ////System.out.println("parseCharArrayLIOR4-3");
        offset = this.skipPreamble(input, offset, end, currentLineNr);
        ////System.out.println("parseCharArrayLIOR4-4");
        offset = this.scanTagName(input, offset, end, currentLineNr);
        ////System.out.println("parseCharArrayLIOR4-5");
        //this.lineNr = currentLineNr[0];
        ////System.out.println("parseCharArrayLIOR4-6");

        //System.out.println("parseCharArrayLIOR4-6-length=" + input.length + "-" + offset + "-" + end + "-" + currentLineNr);

        offset = this.scanAttributes(input, offset, end, currentLineNr);

        //System.out.println("parseCharArrayLIOR4-7");
        int[] contentOffset = new int[1];
        ////System.out.println("parseCharArrayLIOR4-8");
        int[] contentSize = new int[1];
        ////System.out.println("parseCharArrayLIOR4-9");
        int contentLineNr = currentLineNr[0];
        ////System.out.println("parseCharArrayLIOR4-10");
        offset = this.scanContent(input, offset, end,
                contentOffset, contentSize, currentLineNr);
        ////System.out.println("parseCharArrayLIOR4-11");

        if (contentSize[0] > 0)
        {
            this.scanChildren(input, contentOffset[0], contentSize[0],
                    contentLineNr);

            if (this.children.size() > 0)
            {
                this.contents = null;
            }
            else
            {
                this.processContents(input, contentOffset[0],
                        contentSize[0], contentLineNr);
            }
        }
        ////System.out.println("parseCharArrayLIOR4-12");
        return offset;
    }

    /**
     * Decodes the entities in the contents and, if skipLeadingWhitespace is
     * <CODE>true</CODE>, removes extraneous whitespaces after newlines and
     * convert those newlines into spaces.
     *
     * @see nanoxml.kXMLElement#decodeString
     *
     * @exception nanoxml.Exception
     *    if an error occured while parsing the array
     */
    private void processContents(char[] input,
                                 int contentOffset,
                                 int contentSize,
                                 int contentLineNr)
            throws Exception
    {
        int[] lineNr = new int[1];
        lineNr[0] = contentLineNr;

        if (!this.skipLeadingWhitespace)
        {
            String str = new String(input, contentOffset, contentSize);
            this.contents = this.decodeString(str, lineNr[0]);
            return;
        }

        StringBuffer result = new StringBuffer(contentSize);
        int end = contentSize + contentOffset;

        for (int i = contentOffset; i < end; i++)
        {
            char ch = input[i];

            // The end of the contents is always a < character, so there's
            // no danger for bounds violation
            while ((ch == '\r') || (ch == '\n'))
            {
                lineNr[0]++;
                result.append(ch);

                i++;
                ch = input[i];

                if (ch != '\n')
                {
                    result.append(ch);
                }

                do
                {
                    i++;
                    ch = input[i];
                }
                while ((ch == ' ') || (ch == '\t'));
            }

            if (i < end)
            {
                result.append(ch);
            }
        }

        this.contents = this.decodeString(result.toString(), lineNr[0]);
    }

    /**
     * Scans the attributes of the object.
     *
     * @return the offset in the string following the attributes, so that
     *         input[offset] in { '/', '>' }
     *
     * @see nanoxml.kXMLElement#scanOneAttribute
     *
     * @exception nanoxml.Exception
     *    if an error occured while parsing the array
     */
    private int scanAttributes(char[] input,
                               int offset,
                               int end,
                               int[] lineNr)
            throws Exception
    {


        //lior

        //System.out.println("LIORscanAttributes - 1");
        //String key, value;

        for (;;)
        {
            offset = this.skipWhitespace(input, offset, end, lineNr);

            char ch = input[offset];

            if ((ch == '/') || (ch == '>'))
            {
                break;
            }

            offset = this.scanOneAttribute(input, offset, end, lineNr);
        }

        return offset;
    }

    /**!!!
     * Searches the content for child objects. If such objects exist, the
     * content is reduced to <CODE>null</CODE>.
     *
     * @see nanoxml.kXMLElement#parseCharArray
     *
     * @exception nanoxml.Exception
     *    if an error occured while parsing the array
     */
    protected void scanChildren(char[] input,
                                int contentOffset,
                                int contentSize,
                                int contentLineNr)
            throws Exception
    {
        int end = contentOffset + contentSize;
        int offset = contentOffset;
        int lineNr[] = new int[1];
        lineNr[0] = contentLineNr;

        while (offset < end)
        {
            try
            {
                offset = this.skipWhitespace(input, offset, end, lineNr);
            }
            catch (Exception e)
            {
                return;
            }

            if ((input[offset] != '<') || ((input[offset + 1] == '!') && (input[offset + 2] == '[')))
            {
                return;
            }

            kXMLElement child = this.createAnotherElement();
            offset = child.parseCharArray(input, offset, end, lineNr);
            this.children.addElement(child);
        }
    }

    /**
     * Creates a new XML element.
     */
    protected kXMLElement createAnotherElement()
    {
        return new kXMLElement(this.conversionTable,
                this.skipLeadingWhitespace,
                false,
                this.ignoreCase);
    }

    /**
     * Scans the content of the object.
     *
     * @return the offset after the XML element; contentOffset points to the
     *         start of the content section; contentSize is the size of the
     *         content section
     *
     * @exception nanoxml.Exception
     *    if an error occured while parsing the array
     */
    private int scanContent(char[] input,
                            int offset,
                            int end,
                            int[] contentOffset,
                            int[] contentSize,
                            int[] lineNr)
            throws Exception
    {
        if (input[offset] == '/')
        {
            contentSize[0] = 0;

            if (input[offset + 1] != '>')
            {
                throw this.expectedInput("'>'", lineNr[0]);
            }

            return offset + 2;
        }

        if (input[offset] != '>')
        {
            throw this.expectedInput("'>'", lineNr[0]);
        }

        if (this.skipLeadingWhitespace)
        {
            offset = this.skipWhitespace(input, offset + 1, end, lineNr);
        }
        else
        {
            offset++;
        }

        //int begin = offset;
        contentOffset[0] = offset;
        int level = 0;
        char[] tag = this.tagName.toCharArray();
        end -= (tag.length + 2);

        while ((offset < end) && (level >= 0))
        {
            if (input[offset] == '<')
            {
                boolean ok = true;

                if ((offset < (end - 1)) && (input[offset + 1] == '!'))
                {
                    offset++;
                    continue;
                }

                for (int i = 0; ok && (i < tag.length); i++)
                {
                    ok &= (input[offset + (i + 1)] == tag[i]);
                }

                ok &= !this.isIdentifierChar(input[offset + tag.length + 1]);

                if (ok)
                {
                    while ((offset < end) && (input[offset] != '>'))
                    {
                        offset++;
                    }

                    if (input[offset - 1] != '/')
                    {
                        level++;
                    }

                    continue;
                }
                else if (input[offset + 1] == '/')
                {
                    ok = true;

                    for (int i = 0; ok && (i < tag.length); i++)
                    {
                        ok &= (input[offset + (i + 2)] == tag[i]);
                    }

                    if (ok)
                    {
                        contentSize[0] = offset - contentOffset[0];
                        offset += tag.length + 2;
                        offset = this.skipWhitespace(input, offset, end,
                                lineNr);

                        if (input[offset] == '>')
                        {
                            level--;
                            offset++;
                        }

                        continue;
                    }
                }
            }

            if (input[offset] == '\r')
            {
                lineNr[0]++;

                if ((offset != end) && (input[offset + 1] == '\n'))
                {
                    offset++;
                }
            }
            else if (input[offset] == '\n')
            {
                lineNr[0]++;
            }

            offset++;
        }

        if (level >= 0)
        {
            throw this.unexpectedEndOfData(lineNr[0]);
        }

        if (this.skipLeadingWhitespace)
        {
            int i = contentOffset[0] + contentSize[0] - 1;

            while ((contentSize[0] >= 0) && (input[i] <= ' '))
            {
                i--;
                contentSize[0]--;
            }
        }

        return offset;
    }

    /**
     * Scans an identifier.
     *
     * @return the identifier, or <CODE>null</CODE> if offset doesn't point
     *         to an identifier
     */
    private String scanIdentifier(char[] input,
                                  int offset,
                                  int end)
    {
        int begin = offset;

        while ((offset < end) && (this.isIdentifierChar(input[offset])))
        {
            offset++;
        }

        if ((offset == end) || (offset == begin))
        {
            return null;
        }
        else
        {
            return new String(input, begin, offset - begin);
        }
    }

    /**
     * Scans one attribute of an object.
     *
     * @return the offset after the attribute
     *
     * @exception nanoxml.Exception
     *    if an error occured while parsing the array
     */
    private int scanOneAttribute(char[] input,
                                 int offset,
                                 int end,
                                 int[] lineNr)
            throws Exception
    {
        String key, value;

        key = this.scanIdentifier(input, offset, end);

        if (key == null)
        {
            throw this.syntaxError("an attribute key", lineNr[0]);
        }

        offset = this.skipWhitespace(input, offset + key.length(), end, lineNr);

        if (this.ignoreCase)
        {
            key = key.toUpperCase();
        }

        if (input[offset] != '=')
        {
            throw this.valueMissingForAttribute(key, lineNr[0]);
        }

        offset = this.skipWhitespace(input, offset + 1, end, lineNr);

        value = this.scanString(input, offset, end, lineNr);

        if (value == null)
        {
            throw this.syntaxError("an attribute value", lineNr[0]);
        }

        if ((value.charAt(0) == '"') || (value.charAt(0) == '\''))
        {
            value = value.substring(1, (value.length() - 1));
            offset += 2;
        }

        this.attributes.put(key, this.decodeString(value, lineNr[0]));
        return offset + value.length();
    }

    /**
     * Scans a string. Strings are either identifiers, or text delimited by
     * double quotes.
     *
     * @return the string found, without delimiting double quotes; or null
     *         if offset didn't point to a valid string
     *
     * @see nanoxml.kXMLElement#scanIdentifier
     *
     * @exception nanoxml.Exception
     *    if an error occured while parsing the array
     */
    private String scanString(char[] input,
                              int offset,
                              int end,
                              int[] lineNr)
            throws Exception
    {
        char delim = input[offset];

        if ((delim == '"') || (delim == '\''))
        {
            int begin = offset;
            offset++;

            while ((offset < end) && (input[offset] != delim))
            {
                if (input[offset] == '\r')
                {
                    lineNr[0]++;

                    if ((offset != end) && (input[offset + 1] == '\n'))
                    {
                        offset++;
                    }
                }
                else if (input[offset] == '\n')
                {
                    lineNr[0]++;
                }

                offset++;
            }

            if (offset == end)
            {
                return null;
            }
            else
            {
                return new String(input, begin, offset - begin + 1);
            }
        }
        else
        {
            return this.scanIdentifier(input, offset, end);
        }
    }

    /**
     * Scans the class (tag) name of the object.
     *
     * @return the position after the tag name
     *
     * @exception nanoxml.Exception
     *    if an error occured while parsing the array
     */
    private int scanTagName(char[] input,
                            int offset,
                            int end,
                            int[] lineNr)
            throws Exception
    {
        this.tagName = this.scanIdentifier(input, offset, end);

        if (this.tagName == null)
        {
            throw this.syntaxError("a tag name", lineNr[0]);
        }

        return offset + this.tagName.length();
    }

    /**
     * Skips a tag that don't contain any useful data: &lt;?...?&gt;,
     * &lt;!...&gt; and comments.
     *
     * @return the position after the tag
     *
     * @exception nanoxml.Exception
     *    if an error occured while parsing the array
     */
    protected int skipBogusTag(char[] input,
                               int offset,
                               int end,
                               int[] lineNr) throws Exception
    {
        if ((input[offset + 1] == '-') && (input[offset + 2] == '-'))
        {
            while ((offset < end) && ((input[offset] != '-') || (input[offset + 1] != '-') || (input[offset + 2] != '>')))
            {
                if (input[offset] == '\r')
                {
                    lineNr[0]++;

                    if ((offset != end) && (input[offset + 1] == '\n'))
                    {
                        offset++;
                    }
                }
                else if (input[offset] == '\n')
                {
                    lineNr[0]++;
                }

                offset++;
            }


            if (offset == end)
            {
                throw unexpectedEndOfData(lineNr[0]);
            }
            else
            {
                return offset + 3;
            }
        }

        int level = 1;

        while (offset < end)
        {
            char ch = input[offset++];

            switch (ch)
            {
                case '\r':
                    if ((offset < end) && (input[offset] == '\n'))
                    {
                        offset++;
                    }

                    lineNr[0]++;
                    break;

                case '\n':
                    lineNr[0]++;
                    break;

                case '<':
                    level++;
                    break;

                case '>':
                    level--;

                    if (level == 0)
                    {
                        return offset;
                    }

                    break;

                default:
            }
        }

        throw this.unexpectedEndOfData(lineNr[0]);
    }

    /**
     * Skips a tag that don't contain any useful data: &lt;?...?&gt;,
     * &lt;!...&gt; and comments.
     *
     * @return the position after the tag
     *
     * @exception nanoxml.Exception
     *    if an error occured while parsing the array
     */
    private int skipPreamble(char[] input,
                             int offset,
                             int end,
                             int[] lineNr)
            throws Exception
    {
        char ch;

        do
        {
            offset = this.skipWhitespace(input, offset, end, lineNr);

            if (input[offset] != '<')
            {
                this.expectedInput("'<'", lineNr[0]);
            }

            offset++;

            if (offset >= end)
            {
                throw this.unexpectedEndOfData(lineNr[0]);
            }

            ch = input[offset];

            if ((ch == '!') || (ch == '?'))
            {
                offset = this.skipBogusTag(input, offset, end, lineNr);
            }
        }
        while (!isIdentifierChar(ch));

        return offset;
    }

    /**
     * Skips whitespace characters.
     *
     * @return the position after the whitespace
     *
     * @exception nanoxml.Exception
     *    if an error occured while parsing the array
     */
    private int skipWhitespace(char[] input,
                               int offset,
                               int end,
                               int[] lineNr) throws Exception
    {
        while ((offset < end) && (input[offset] <= ' '))
        {
            if (input[offset] == '\r')
            {
                lineNr[0]++;

                if ((offset != end) && (input[offset + 1] == '\n'))
                {
                    offset++;
                }
            }
            else if (input[offset] == '\n')
            {
                lineNr[0]++;
            }

            offset++;
        }

        if (offset == end)
        {
            throw this.unexpectedEndOfData(lineNr[0]);
        }

        return offset;
    }

    /**
     * Converts &amp;...; sequences to "normal" chars.
     */
    protected String decodeString(String s,
                                  int lineNr)
    {
        StringBuffer result = new StringBuffer(s.length());
        int index = 0;

        while (index < s.length())
        {
            int index2 = (s + '&').indexOf('&', index);
            int index3 = (s + "<![CDATA[").indexOf("<![CDATA[", index);

            if (index2 <= index3)
            {
                result.append(s.substring(index, index2));

                if (index2 == s.length())
                {
                    break;
                }

                index = s.indexOf(';', index2);

                if (index < 0)
                {
                    result.append(s.substring(index2));
                    break;
                }

                String key = s.substring(index2 + 1, index);

                if (key.charAt(0) == '#')
                {
                    if (key.charAt(1) == 'x')
                    {
                        result.append((char) (Integer.parseInt(key.substring(2),
                                16)));
                    }
                    else
                    {
                        result.append((char) (Integer.parseInt(key.substring(1),
                                10)));
                    }
                }
                else
                {
                    result.append(this.conversionTable.getProperty(key, "&" + key + ';'));
                }
            }
            else
            {
                int index4 = (s + "]]>").indexOf("]]>", index3 + 9);
                result.append(s.substring(index, index3));
                result.append(s.substring(index3 + 9, index4));
                index = index4 + 2;
            }

            index++;
        }

        return result.toString();
    }

    /**
     * The end of the data input has been reached.
     */
    private Exception unexpectedEndOfData(int lineNr)
    {
        String msg = "Unexpected end of data";
        return new Exception("NanoXML | " + this.getTagName() + " | " + lineNr + " | " + msg);
    }

    /**
     * A syntax error occured.
     */
    private Exception syntaxError(String context,
                                  int lineNr)
    {
        String msg = "Syntax error  " + context;
        return new Exception("NanoXML | " + this.getTagName() + " | " + lineNr + " | " + msg);
    }

    /**
     * A character has been expected.
     */
    private Exception expectedInput(String charSet,
                                    int lineNr)
    {
        String msg = "Expected: " + charSet;
        return new Exception("NanoXML | " + this.getTagName() + " | " + lineNr + " | " + msg);
    }

    /**
     * A value is missing for an attribute.
     */
    private Exception valueMissingForAttribute(String key,
                                               int lineNr)
    {
        String msg = "Value missing for attribute  \"" + key + "\"";
        return new Exception("NanoXML | " + this.getTagName() + " | " + lineNr + " | " + msg);
    }

    public String reGenerateXmlNode()
    {

        Enumeration attribsEnum = this.attributes.keys();
        StringBuffer sb = new StringBuffer();

        String property;
        int childSize = children.size();
        boolean isEmptyTag = (childSize == 0 && contents != null && contents.equals("") == true);

        sb.append("<");
        sb.append(this.tagName);
        while (attribsEnum.hasMoreElements() == true)
        {
            property = attribsEnum.nextElement().toString();
            sb.append(" ");
            sb.append(property);
            sb.append("=");
            sb.append("\"");
            sb.append(this.getProperty(property, ""));
            sb.append("\"");
        }

        if (isEmptyTag == true)
        {
            sb.append(" />");
        }
        else
        {
            sb.append(">");
            if (childSize == 0) // have content text
            {
                sb.append(contents);
            }
            else
            {
                kXMLElement node;
                for (int i = 0; i < childSize; i++)
                {
                    node = (kXMLElement) children.elementAt(i);
                    sb.append(node.reGenerateXmlNode());
                }
            }

            sb.append("</");
            sb.append(this.tagName);
            sb.append(">");
        }

        return sb.toString();
    }
}

